create-*命令,实际上Grails最后都会自动帮它们创建集成好的全部测试实例。
比如你运行下方的create-controller
命令:
grails create-controller simple
Grails不仅在grails-app/controllers/目录下创建了SimpleController.groovy,而且在
test/integration/目录下创建了对它的集成测试实例
SimpleControllerTests.groovy。
,然而Grails不会在这个测试实例里自动生成逻辑代码,这部分需要你自己写。
grails test-app
------------------------------------------------------- Running Unit Tests… Running test FooTests...FAILURE Unit Tests Completed in 464ms … -------------------------------------------------------Tests failed: 0 errors, 1 failures
grails test-app SimpleController
grails test-app SimpleController BookController
BookController调用如下的一个服务应用:
class MyService { def otherService String createSomething() { def stringId = otherService.newIdentifier() def item = new Item(code: stringId, name: "Bangle") item.save() return stringId } int countItems(String name) { def items = Item.findAllByName(name) return items.size() } }
grails.test.GrailsUnitTestCase类。
它是
GroovyTestCase子类,为Grails应用和组件提供测试工具。这个类为模拟
特殊类型提供了若干方法,并且提供了按Groovy的MockFor和StubFor方式模拟的支持。
正常来说你在看之前所示的MyService例子和它对另外一个应用服务的依赖,以及例子中使用到的动态域类方法会有一点痛苦。你可以在这个例子中使用元类编程和“map
as object”规则,但是很快你会发现使用这些方法会变得很糟糕,那我们要怎么用GrailsUnitTestCase写它的测试呢?
import grails.test.GrailsUnitTestCaseclass MyServiceTests extends GrailsUnitTestCase { void testCreateSomething() { // Mock the domain class. def testInstances = [] mockDomain(Item, testInstances) // Mock the "other" service. String testId = "NH-12347686" def otherControl = mockFor(OtherService) otherControl.demand.newIdentifier(1..1) {-> return testId } // Initialise the service and test the target method. def testService = new MyService() testService.otherService = otherControl.createMock() def retval = testService.createSomething() // Check that the method returns the identifier returned by the // mock "other" service and also that a new Item instance has // been saved. assertEquals testId, retval assertEquals 1, testInstances assertTrue testInstances[0] instanceof Item } void testCountItems() { // Mock the domain class, this time providing a list of test // Item instances that can be searched. def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"), new Item(code: "EC-4395734", name: "Lamp"), new Item(code: "TF-4927324", name: "Laptop") ] mockDomain(Item, testInstances) // Initialise the service and test the target method. def testService = new MyService() assertEquals 2, testService.countItems("Laptop") assertEquals 1, testService.countItems("Lamp") assertEquals 0, testService.countItems("Chair") } }
mockDomain()方法,这是
GrailsUnitTestCase类提供的其中一个方法:
def testInstances = [] mockDomain(Item, testInstances)
下面我们将重点讲解mockFor方法:
def otherControl = mockFor(OtherService)
otherControl.demand.newIdentifier(1..1) {-> return testId }
def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"), new Item(code: "EC-4395734", name: "Lamp"), new Item(code: "TF-4927324", name: "Laptop") ] mockDomain(Item, testInstances)
一些介绍GrailsUnitTestCase中mock..()方法的例子。
在这部分我们将详细地介绍所有GrailsUnitTestCase中
提供的方法,首先以通用的mockFor()开始。在开始之前,有一个很重要的说明先说一下,使用这些方法可以保证对所给的类做出的任何改变都不会让其他测试实例受影响。这里有个普遍出现且严重的问题,当你尝试通过meta-class编程方法对它自身进行模拟,但是只要你对每个想模拟的类使用任何一个mock..()
方法,这个问题就会消失了。
mockFor(class, loose = false)
def strictControl = mockFor(MyService) strictControl.demand.someMethod(0..2) { String arg1, int arg2 -> … } strictControl.demand.static.aStaticMethod {-> … }
mockControl.createMock()。事实上,你可以调用这个方法生成你想要的任何数量的mock实例。一旦执行了test方法,你就可以调用
mockControl.verify()方法检查你想要执行的方法执行了没。
最后,如下这个调用:
def looseControl = mockFor(MyService, true)
可以用mockForConstraintsTests()解决这个问题。
这个方法就像mockDomain()方法的简化版本,简单得对所给的domain类添加一个validate()方法。你所要做的就是mock这个类,创建带有属性值的实例,然后调用validate()方法。你可以查看domain实例的errors属性判断这个确认方法是否失败。所以假如所有我们正在做的是模拟validate()方法,那么可选的测试实例数组参数呢?这就是我们为什么可以测试唯一约束的原因,你很快就可以看见了。
那么假设我们拥有如下的一个简单domain类:
class Book { String title String author static constraints = { title(blank: false, unique: true) author(blank: false, minSize: 5) } }
class BookTests extends GrailsUnitTestCase { void testConstraints() { def existingBook = new Book(title: "Misery", author: "Stephen King") mockForConstraintsTests(Book, [ existingBook ]) // Validation should fail if both properties are null. def book = new Book() assertFalse book.validate() assertEquals "nullable", book.errors["title"] assertEquals "nullable", book.errors["author"] // So let's demonstrate the unique and minSize constraints. book = new Book(title: "Misery", author: "JK") assertFalse book.validate() assertEquals "unique", book.errors["title"] assertEquals "minSize", book.errors["author"] // Validation should pass! book = new Book(title: "The Shining", author: "Stephen King") assertTrue book.validate() } }
你可以在没有进一步解释的情况下,阅读上面这些代码,思考它们正在做什么事情。我们会解释的唯一一件事是errors属性使用的方式。第一,它返回了真实的Spring Errors实例,所以你可以得到你通常期望的所有属性和方法。第二,这个特殊的Errors对象也可以用如上map/property方式使用。简单地读取你感兴趣的属性名字,map/property接口会返回被确认的约束名字。注意它是约束的名字,不是你所期望的信息内容。
这是测试约束讲解部分。我们要讲的最后一件事是用这种方式测试约束会捕捉一个共同的错误:typos in the "constraints" property。正常情况下这是目前最难捕捉的一个bug,还没有一个约束单元测试可以直接简单得发现这个问题。ControllerUnitTestCase一起连用。
TagLibUnitTestCase
一起连用。
实质上,Grails自动用 MockHttpServletRequest,MockHttpServletResponse,和 MockHttpSession 配置每个测试实例,你可以使用它们执行你的测试用例。比如你可以考虑如下controller:
class FooController { def text = { render "bar" } def someRedirect = { redirect(action:"bar") } }
class FooControllerTests extends GroovyTestCase { void testText() { def fc = new FooController() fc.text() assertEquals "bar", fc.response.contentAsString } void testSomeRedirect() { def fc = new FooController() fc.someRedirect() assertEquals "/foo/bar", fc.response.redirectedUrl } }
MockHttpServletResponse实例,你可以使用这个实例获取写进返回对象的contentAsString值,或是跳转的URL。
这些Servlet
API的模拟版本全部都很更改,不像模拟之前那样子,因此你可以对请求对象设置属性,比如contextPath等。
Grails在集成测试期间调用actions不会自动执行interceptors,你要单独测试拦截器,必要的话通过functional
testing测试。
class FilmStarsController {
def popularityService def update = {
// do something with popularityService
}
}
class FilmStarsTests extends GroovyTestCase { def popularityService public void testInjectedServiceInController () { def fsc = new FilmStarsController() fsc.popularityService = popularityService fsc.update() } }
class AuthenticationController { def signup = { SignupForm form -> … } }
def controller = new AuthenticationController() controller.params.login = "marcpalmer" controller.params.password = "secret" controller.params.passwordConfirm = "secret" controller.signup()
def save = { def book = Book(params) if(book.save()) { // handle } else { render(view:"create", model:[book:book]) } }
def bookController = new BookController()
bookController.save()
def model = bookController.modelAndView.model.book
def create = {
[book: new Book(params['book']) ]
}
void testCreateWithXML() { def controller = new BookController() controller.request.contentType = 'text/xml' controller.request.contents = '''<?xml version="1.0" encoding="ISO-8859-1"?> <book> <title>The Stand</title> … </book> '''.getBytes() // note we need the bytes def model = controller.create() assert model.book assertEquals "The Stand", model.book.title }
void testCreateWithJSON() { def controller = new BookController() controller.request.contentType = "text/json" controller.request.content = '{"id":1,"class":"Book","title":"The Stand"}'.getBytes() def model = controller.create() assert model.book assertEquals "The Stand", model.book.title }
使用JSON,也不要忘记对class属性指定名字,绑定的目标类型。在XML里,在book节点内这些设置隐含的,但是使用JSON你需要这个属性作为JSON包的一部分。更多关于REST web应用的信息,可以参考REST章节。
grails.test.WebFlowTestCase,它继承Spring
Web Flow的
AbstractFlowExecutionTests 类。Testing Web Flows
requires a special test harness called grails.test.WebFlowTestCase
which sub classes Spring Web Flow's AbstractFlowExecutionTests class.
WebFlowTestCase子类必须是集成测试实例Subclasses
of WebFlowTestCase
must be
integration tests
例如在下面的这个小flow情况下:
class ExampleController { def exampleFlow = { start { on("go") { flow.hello = "world" }.to "next" } next { on("back").to "start" on("go").to "end" } end() } }
class ExampleFlowTests extends grails.test.WebFlowTestCase { def getFlow() { new ExampleController().exampleFlow } … }
class ExampleFlowTests extends grails.test.WebFlowTestCase { String getFlowId() { "example" } … }
一旦这在你的测试实例里实现了,你需要用startFlow方法开始启动这个flow,这个方法会返回ViewSelection对象:
void testExampleFlow() {
def viewSelection = startFlow() assertEquals "start", viewSelection.viewName
…
}
void testExampleFlow() { … viewSelection = signalEvent("go") assertEquals "next", viewSelection.viewName assertEquals "world", viewSelection.model.hello }
class FooTagLib { def bar = { attrs, body -> out << "<p>Hello World!</p>" } def bodyTag = { attrs, body -> out << "<${attrs.name}>" out << body() out << "</${attrs.name}>" } }
class FooTagLibTests extends GroovyTestCase { void testBarTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bar(null,null) } void testBodyTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bodyTag(name:"p") { "Hello World!" } } }
grails.test.GroovyPagesTestCase类测试标签库。
GroovyPagesTestCase类是常见GroovyTestCase的子类,它为GSP显示输出提供实用方法。
GroovyPagesTestCase类只能在集成测试中使用。
举个时间格式化标签库的例子,如下:
class FormatTagLib {
def dateFormat = { attrs, body ->
out << new java.text.SimpleDateFormat(attrs.format) << attrs.date
}
}
class FormatTagLibTests extends GroovyPagesTestCase { void testDateFormat() { def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date assertOutputEquals( '01-01-2008', template, [myDate:testDate] ) } }
class FormatTagLibTests extends GroovyPagesTestCase { void testDateFormat() { def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date def result = applyTemplate( template, [myDate:testDate] ) assertEquals '01-01-2008', result } }
void testQuery() { def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")] books*.save() assertEquals 2, Book.list().size() }
void testQuery() { def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")] books*.save(flush:true) assertEquals 2, Book.list().size() }
grails install-plugin webtest